Este relatório analisa o treino matinal de remo no Yacht Club da Bahia em 31 de Janeiro de 2026. Os dados de GPS são baixados da plataforma Treinus e os dados ambientais da boia SIMCOSTA 515. O relatório calcula os trechos mais rápidos de 500 m em linha reta e examina como vento e corrente influenciaram cada desempenho.

1 – Pacotes

library(yachtvaa)
library(dplyr)
library(sf)
library(ggplot2)
library(lubridate)

2 – Aquisição de dados

Baixa os registros GPS dos remadores e os dados da boia SIMCOSTA 515 em uma única chamada. Com cache = TRUE os dados são lidos do disco sem necessidade de autenticação.

if (!params$cache) {
 session <- treinusr::treinus_auth()
}
use_cache <- params$cache
raw <- fetch_session_data(
  session = session,
  date        = params$date,
  start_time  = params$start_time,
  end_time    = params$end_time,
  athlete_ids = params$athlete_ids,
  cache       = use_cache,
  overwrite_db=!use_cache,
  use_memoise=FALSE
)

records <- raw$records%>%
  mutate(fullname_athlete = tolower(fullname_athlete))
buoy    <- raw$buoy

# Filtro espacial: manter todos os registros para atletas que têm pelo menos
# alguns pontos dentro da área de estudo (ao invés de manter apenas os
# pontos dentro da bbox)
records_sf <- records_to_sf(records)
study_area <- sf::st_bbox(
  c(xmin = -38.61380, ymin = -13.00741,
    xmax = -38.46308, ymax = -12.81308),
  crs = 4326L
) |>
  sf::st_as_sfc() |>
  sf::st_transform(sf::st_crs(records_sf))

# Identificar atletas que têm pelo menos um ponto dentro da área de estudo
athletes_in_bbox <- records_sf[
  lengths(sf::st_intersects(records_sf, study_area)) > 0,
  "id_athlete"
]

# Manter todos os registros para esses atletas
records_sf <- records_sf[records_sf$id_athlete %in% athletes_in_bbox$id_athlete, ]

# Criar versão filtrada para cálculos de velocidade (apenas pontos dentro da bbox)
records_sf_bbox <- records_sf[
  lengths(sf::st_intersects(records_sf, study_area)) > 0,
]

# Documentação da lógica de filtragem espacial:
# 1. Identificamos atletas que têm pelo menos um ponto dentro da bbox
# 2. Mantemos TODOS os registros desses atletas (não apenas os dentro da bbox)
# 3. Para cálculos de velocidade (como no fast500), usamos apenas pontos dentro da bbox
# 4. Aplicamos pós-filtro no fast500 para garantir início/fim dos segmentos dentro da bbox

3 – Quem estava na água?

Nem todo atleta retornado pelo Treinus realmente remou – alguns podem ter registros GPS breves em terra. Mantemos apenas atletas com pelo menos 100 pontos GPS e percurso acumulado superior a 200 m.

track_summary <- records_sf |>
  st_drop_geometry() |>
  summarise(
    n_fixes      = n(),
    first_fix    = min(timestamp),
    last_fix     = max(timestamp),
    duration_min = as.numeric(difftime(max(timestamp), min(timestamp),
                                       units = "mins")),
    .by = c(id_athlete, fullname_athlete)
  )

# Distância acumulada ponto a ponto por atleta
track_distance <- records_sf |>
  arrange(id_athlete, timestamp) |>
  mutate(
    coords = st_coordinates(geometry),
    x = coords[, 1],
    y = coords[, 2],
    .keep = "unused"
  ) |>
  st_drop_geometry() |>
  summarise(
    track_distance_m = sum(sqrt(diff(x)^2 + diff(y)^2)),
    .by = id_athlete
  )

track_summary <- track_summary |>
  left_join(track_distance, by = "id_athlete")

paddlers <- track_summary |>
  filter(n_fixes >= 100, track_distance_m >= 200)

paddlers |>
  arrange(desc(track_distance_m)) |>
  mutate(
    track_distance_km = round(track_distance_m / 1000, 1),
    duration_min      = round(duration_min, 0),
    speed_kmh = round((track_distance_m/duration_min) * 60 / 1000,1)
  ) |>
  select(fullname_athlete, n_fixes, duration_min, track_distance_km, speed_kmh) |>
  gt::gt()%>%
  gt::cols_label(fullname_athlete="Atleta", 
  n_fixes="Pontos GPS", 
  duration_min="Duração (min)", 
  track_distance_km="Distância (km)", speed_kmh="Velocidade Km/h")%>%
    gt::tab_caption("Atletas identificados na água")
Atletas identificados na água
Atleta Pontos GPS Duração (min) Distância (km) Velocidade Km/h
victor patiri 2001 114 14.0 7.4
eduardo leoni 7477 125 12.1 5.8
eduardo valdes sanchez 887 102 10.3 6.1
fernanda siqueira 807 74 9.9 8.0
ricardo rappel 1027 80 9.4 7.1
verena barbara carneiro 1037 74 9.3 7.6
carita souza 725 64 9.2 8.6
rosário calmon 571 70 9.2 7.9
juliana rappel 727 84 9.2 6.5
maria regina valente 1020 68 9.0 7.9
izabela luz 632 68 8.9 7.9
stela de sá hama 820 68 8.9 7.8
josé barretto 3322 55 8.1 8.9
ana lucia berenguer 718 54 7.9 8.8
records_sf <- records_sf |>
  filter(id_athlete %in% paddlers$id_athlete)

n_paddlers <- nrow(paddlers)

4 – Mapa dos trechos mais rápidos

arrow_df <- fast500 |>
  as_tibble()

basemap <- maptiles::get_tiles(records_sf, provider = "CartoDB.Positron",
                               crop = FALSE, verbose = FALSE)

ggplot() +
  tidyterra::geom_spatraster_rgb(data = basemap) +
  geom_sf(data = records_sf, colour = "grey50", size = 0.1, alpha = 0.4) +
  geom_segment(
    data = arrow_df,
    aes(x = start_x, y = start_y, xend = end_x, yend = end_y,
        colour = avg_speed_kmh),
    arrow = arrow(length = unit(0.3, "cm"), type = "closed"),
    linewidth = 1.3
  ) +
  ggrepel::geom_text_repel(
    data = arrow_df,
    aes(x = (start_x + end_x) / 2, y = (start_y + end_y) / 2,
        label = fullname_athlete),
    size = 2.5, max.overlaps = 20,
    bg.color = "white", bg.r = 0.15
  ) +
  scale_colour_viridis_c(option = "plasma", name = "Vel. (km/h)") +
  coord_sf(crs = sf::st_crs(records_sf)) +
  labs(
    title    = paste("Trechos mais rápidos de 500 m --", date_short),
    subtitle = "Setas indicam a direção do percurso; cor = velocidade do atleta"
  ) +
  theme_void(base_size = 11) +
  theme(
    legend.position = "bottom",
    plot.title      = element_text(face = "bold"),
    plot.margin     = margin(5, 5, 5, 5)
  )
Trechos mais rápidos de 500 m em linha reta. Setas indicam a direção do percurso.

Trechos mais rápidos de 500 m em linha reta. Setas indicam a direção do percurso.

5 – Condições da boia durante o treino

Interpolar os dados da boia em uma grade regular de 10 minutos.

buoy_ip <- interpolate_buoy(buoy)

Vento

has_wind <- "wind_speed" %in% names(buoy_ip) &&
  any(!is.na(buoy_ip$wind_speed))
buoy_wind <- buoy_ip |>
  filter(!is.na(wind_speed)) |>
  mutate(
    wind_speed_kmh = wind_speed * 3.6,
    local_time     = with_tz(datetime, tzone = "America/Bahia")
  )

p_speed <- ggplot(buoy_wind, aes(local_time, wind_speed_kmh)) +
  geom_line(linewidth = 0.8, colour = "#0072B2") +
  geom_point(size = 1.5, colour = "#0072B2") +
  labs(y = "Velocidade do vento (km/h)", x = NULL, title = "Condições de vento") +
  theme_minimal(base_size = 11)

p_dir <- ggplot(buoy_wind, aes(local_time, wind_direction)) +
  geom_line(linewidth = 0.8, colour = "#D55E00") +
  geom_point(size = 1.5, colour = "#D55E00") +
  scale_y_continuous(
    limits = c(0, 360),
    breaks = seq(0, 360, 90),
    labels = c("N", "E", "S", "W", "N")
  ) +
  labs(y = "Direção do vento (de)", x = "Hora local (America/Bahia)") +
  theme_minimal(base_size = 11)

gridExtra::grid.arrange(p_speed, p_dir, ncol = 1)
Velocidade e direção do vento registradas pela boia SIMCOSTA 515.

Velocidade e direção do vento registradas pela boia SIMCOSTA 515.

cat("**Sem dados de vento disponíveis para este treino.** O vento será tratado como zero.\n")

Corrente

buoy_current <- buoy_ip |>
  filter(!is.na(current_speed_kmh)) |>
  mutate(local_time = with_tz(datetime, tzone = "America/Bahia"))

p_cspeed <- ggplot(buoy_current, aes(local_time, current_speed_kmh)) +
  geom_line(linewidth = 0.8, colour = "#009E73") +
  geom_point(size = 1.5, colour = "#009E73") +
  labs(y = "Velocidade da corrente (km/h)", x = NULL, title = "Condições de corrente") +
  theme_minimal(base_size = 11)

p_cdir <- ggplot(buoy_current, aes(local_time, current_direction)) +
  geom_line(linewidth = 0.8, colour = "#CC79A7") +
  geom_point(size = 1.5, colour = "#CC79A7") +
  scale_y_continuous(
    limits = c(0, 360),
    breaks = seq(0, 360, 90),
    labels = c("N", "E", "S", "W", "N")
  ) +
  labs(y = "Direção da corrente (para)", x = "Hora local (America/Bahia)") +
  theme_minimal(base_size = 11)

gridExtra::grid.arrange(p_cspeed, p_cdir, ncol = 1)
Velocidade e direção da corrente registradas pela boia 515.

Velocidade e direção da corrente registradas pela boia 515.

6 – Condições nos trechos mais rápidos

Associar cada segmento à observação mais próxima da boia via rolling join e calcular ângulos relativos de vento/corrente.

buoy_for_match <- buoy_ip |>
  transmute(
    datetime,
    wind_direction_deg    = if ("wind_direction" %in% names(buoy_ip))
      wind_direction else NA_real_,
    wind_speed_kmh        = if ("wind_speed" %in% names(buoy_ip))
      wind_speed * 3.6 else NA_real_,
    current_direction_deg = current_direction,
    current_speed_kmh     = current_speed_kmh,
    wave_height,
    wave_direction
  )

matched <- match_buoy_to_segments(fast500, buoy_for_match)

Calcular ângulos relativos de vento/corrente e componentes ao longo do percurso. Quando dados de vento não estão disponíveis, são imputados como zero.

conditions <- apparent_conditions(matched, impute_missing_wind = !has_wind)

Corrente relativa ao rumo

Direção da corrente em relação ao rumo do trecho mais rápido de cada atleta. Topo = corrente a favor (de popa), base = corrente contrária (de proa). O raio indica a intensidade da corrente.

current_polar <- conditions |>
  as_tibble() |>
  filter(!is.na(current_speed_kmh)) |>
  mutate(
    current_relative_deg = (current_direction_deg - bearing_deg) %% 360
  )

current_r_max <- max(5, max(current_polar$current_speed_kmh, na.rm = TRUE))

ggplot(current_polar, aes(x = current_relative_deg, y = current_speed_kmh)) +
  geom_point(aes(colour = avg_speed_kmh), size = 3) +
  ggrepel::geom_text_repel(
    aes(label = fullname_athlete),
    size = 2.5, max.overlaps = 20
  ) +
  coord_polar(start = 0, direction = -1) +
  scale_x_continuous(
    limits = c(0, 360),
    breaks = c(0, 90, 180, 270),
    labels = c("A favor", "90\u00b0", "Contra", "270\u00b0")
  ) +
  scale_y_continuous(limits = c(0, current_r_max)) +
  scale_colour_viridis_c(option = "plasma", name = "Vel. atleta\n(km/h)") +
  labs(
    title    = "Corrente relativa ao rumo do trecho mais rápido",
    subtitle = "Topo = a favor, base = contrária; cor = velocidade do atleta",
    x = NULL,
    y = "Vel. corrente (km/h)"
  ) +
  theme_minimal(base_size = 11)
Corrente relativa ao rumo: topo = a favor, base = contrária. Raio = velocidade da corrente.

Corrente relativa ao rumo: topo = a favor, base = contrária. Raio = velocidade da corrente.

Vento relativo ao rumo

Mesma lógica aplicada ao vento. Topo = vento a favor (de popa), base = vento contrário (de proa).

wind_polar <- conditions |>
  as_tibble() |>
  filter(!is.na(wind_speed_kmh)) |>
  mutate(
    wind_relative_deg = (wind_direction_deg + 180 - bearing_deg) %% 360
  )

wind_r_max <- max(30, max(wind_polar$wind_speed_kmh, na.rm = TRUE))

ggplot(wind_polar, aes(x = wind_relative_deg, y = wind_speed_kmh)) +
  geom_point(aes(colour = avg_speed_kmh), size = 3) +
  ggrepel::geom_text_repel(
    aes(label = fullname_athlete),
    size = 2.5, max.overlaps = 20
  ) +
  coord_polar(start = 0, direction = -1) +
  scale_x_continuous(
    limits = c(0, 360),
    breaks = c(0, 90, 180, 270),
    labels = c("A favor", "90\u00b0", "Contra", "270\u00b0")
  ) +
  scale_y_continuous(limits = c(0, wind_r_max)) +
  scale_colour_viridis_c(option = "plasma", name = "Vel. atleta\n(km/h)") +
  labs(
    title    = "Vento relativo ao rumo do trecho mais rápido",
    subtitle = "Topo = a favor, base = contrário; cor = velocidade do atleta",
    x = NULL,
    y = "Vel. vento (km/h)"
  ) +
  theme_minimal(base_size = 11)
Vento relativo ao rumo: topo = a favor, base = contrário. Raio = velocidade do vento.

Vento relativo ao rumo: topo = a favor, base = contrário. Raio = velocidade do vento.

cat("**Sem dados de vento disponíveis.** Gráfico polar de vento omitido.\n")

7 – Classificação

league <- build_league_table(conditions, athlete_col = "fullname_athlete")
league_fmt <- format_league(league, top_n = 15)

league_fmt |>
  select(
    rank, fullname_athlete, predicted_time_fmt, avg_speed_kmh,
    wind_class, wind_component_kmh,
    current_class, current_component_kmh
  ) |>
  mutate(
    avg_speed_kmh         = round(avg_speed_kmh, 1),
    wind_component_kmh    = round(wind_component_kmh, 1),
    current_component_kmh = round(current_component_kmh, 1)
  ) |>
  knitr::kable(
    col.names = c(
      "Pos.", "Atleta", "Tempo", "Vel. (km/h)",
      "Vento", "Vento (km/h)", "Corrente", "Corrente (km/h)"
    ),
    caption = paste("Classificação: 500 m mais rápidos em", date_short)
  )
Classificação: 500 m mais rápidos em 31 Jan 2026
Pos. Atleta Tempo Vel. (km/h) Vento Vento (km/h) Corrente Corrente (km/h)
1 fernanda siqueira 2:43.2 11.0 tailwind 13.6 cross_left 1.0
2 josé barretto 2:52.8 10.4 headwind -12.7 opposing -1.7
3 carita souza 2:57.1 10.2 tailwind 13.7 following 1.1
4 victor patiri 3:01.8 9.9 headwind -12.6 opposing -1.5
5 ana lucia berenguer 3:03.6 9.8 headwind -13.4 opposing -1.6
6 verena barbara carneiro 3:06.8 9.6 headwind -13.6 opposing -1.5
7 rosário calmon 3:07.7 9.6 headwind -12.4 opposing -1.5
8 maria regina valente 3:12.9 9.3 headwind -13.5 opposing -1.5
9 izabela luz 3:13.3 9.3 headwind -12.4 opposing -1.5
10 stela de sá hama 3:16.2 9.2 headwind -13.6 opposing -1.5
11 ricardo rappel 3:21.9 8.9 headwind -13.9 opposing -1.4
12 eduardo leoni 3:37.7 8.3 crosswind_left 3.7 following 1.3
13 juliana rappel 3:41.2 8.1 headwind -12.8 opposing -1.5
14 eduardo valdes sanchez 3:49.4 7.8 headwind -13.4 cross_right -0.2

8 – Resumo do treino

current_summary <- buoy_current |>
  summarise(
    mean_speed = round(mean(current_speed_kmh, na.rm = TRUE), 2),
    max_speed  = round(max(current_speed_kmh, na.rm = TRUE), 2),
    mean_dir   = round(mean(current_direction, na.rm = TRUE), 0)
  )

winner      <- league_fmt$fullname_athlete[1]
winner_time <- league_fmt$predicted_time_fmt[1]
winner_kmh  <- round(league_fmt$avg_speed_kmh[1], 1)

if (has_wind) {
  wind_summary <- buoy_wind |>
    summarise(
      mean_speed = round(mean(wind_speed_kmh, na.rm = TRUE), 1),
      max_speed  = round(max(wind_speed_kmh, na.rm = TRUE), 1),
      mean_dir   = round(mean(wind_direction, na.rm = TRUE), 0)
    )
  wind_line <- sprintf(
    "- **Vento:** média %s km/h (máx %s), predominante de %s°",
    wind_summary$mean_speed, wind_summary$max_speed, wind_summary$mean_dir
  )
} else {
  wind_line <- "- **Vento:** sem dados disponíveis (imputado como zero)"
}

cat(sprintf(
  "**Resumo do treino:**\n\n
- **Data:** %s
- **Atletas na água:** %d
%s
- **Corrente:** média %s km/h (máx %s), fluindo para %s°
- **500 m mais rápido:** %s em %s (%s km/h)\n",
  date_label,
  n_paddlers,
  wind_line,
  current_summary$mean_speed, current_summary$max_speed, current_summary$mean_dir,
  winner, winner_time, winner_kmh
))

Resumo do treino:

  • Data: 31/01/2026
  • Atletas na água: 14
  • Vento: média 14.7 km/h (máx 28.1), predominante de 119°
  • Corrente: média 0.75 km/h (máx 2.02), fluindo para 156°
  • 500 m mais rápido: fernanda siqueira em 2:43.2 (11 km/h)